接下來就是準備要陸續進入實作環節的部份了。筆者曾在網上一篇文章看過,軟體開發有幾種模式,傳統上以完成整體設計,然後根據細節設計、實現。就好像一個已經技術成熟、胸有成竹的工匠可以穩定的拿著設計圖就直接施工,然而這是一個擁有豐富經驗的工匠可以採取的做法。筆者畢竟是學習者,更偏向另外一種以探索為主的開發方法,不斷試錯、不斷摸索道路。
雖然是這麼說,不過我仍然想在開始之前先來做一個小小的規劃。接下來我會先盤點遊戲引擎系統組件一些較為核心的功能,不只作為之後實作時會著重的地方,也是為將要摸索的道路先做上一些記號,如此一來雖然是以嘗試代替計畫,卻仍然不會偏離原來所設想。
遊戲引擎是一個複雜的軟體架構,就像我們先前介紹的有許多子系統。不同的子系統間會有依賴關係,也因此對於不同子系統間啟動的順序就被隱式的決定了。
首先我們會先想到的是C++的建構函式與解構函式
class RenderManager
{
public:
RenderManager() {}
~RenderManager() {}
};
static RenderManager gRenderManager;
然而仔細想想後,會發現這是行不通的。我們在先前就提到,子系統是有依賴關係的,而C++中的全域或是靜態實例會在進入main()之前被建構,而他們所調用的順序是無法決定的。因此建構與解構函式是不適合來對子系統初始化的。
而這個問題其實有個簡單又快速的解決方法。
class RenderManager
{
public:
RenderManager() {}
~RenderManager() {}
void startUp() {/* 啟動管理器 */}
void shutDown() {/* 終止管理器 */}
};
RenderManager gRenderManager;
int main(int argc,char* argv)
{
gRenderManager.startUp();
// 運行遊戲
gRenderManager.shutDown();
}
雖然還有其他的實現方法,但是筆者認為這種方法既簡單又明確,在摸索之初,以這種簡單粗暴的方法總是在調試或是維護時更能顯現出錯誤點。
遊戲由許多子系統所構成,包含輸入、輸出、渲染、動畫、物理、AI等等。而在遊戲運行時通常都會有週期性的更新,或是30Hz又或是60Hz,在物理動力學模擬的部分可能還需要更高更新頻率、而AI的Brain則或許只要每秒1、2次的更新就好了。而這種使各個引擎子系統週期性更新的循環,也被稱為遊戲循環(Game Loop)。
這裡下面也稍微介紹一下常見的循環框架。
while(true)
{
MSG msg;
while(PeekMessage(&msg,NULL,0,0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
RunOneIterationOFGameLoop();
}
以下使用代碼來舉例。
class frameListener
{
public:
virtual void frameStarted()
{
//渲染之前所需要執行的事情
}
virtual void frameEnded()
{
//渲染之後所需要執行的事情
}
};
while(true)
{
for(auto &frameListener:frameListener)
{
frameListener.frameStarted();
}
renderCurremtScene();
for(auto &frameListener:frameListener)
{
frameListener.frameEnded();
}
finalizeSceneAndSwapBuffers();
}
在早期的遊戲,遊戲循環中並沒有與真實時間有關聯,就像上面幾個框架中,循環會在執行結束後直接開始下一次的循環。這也會導致遊戲在不同的CPU影響而有著不同的執行速度。
這裡是一個小例子,這是筆者之前訓練貪吃蛇DQN的小專題,為了測試成效用了C++來刻一個可視化的環境。可以清楚的看見,上圖是筆者換了新電腦後運行的情況,而下圖則是舊筆電的執行狀況,可以明顯地看見,遊戲並未更改,然而上圖就像是快進了一樣。
而我們也能實現透過一個「時鐘」的概念來完成它。
// 設定一個理想的一幀的時間
F32 dtSeconds = 1.0f / 30.0f;
// 先讀取當前時間
U64 tBegin = readHiResTimer();
while(true)
{
runOneIterationOfGameLoop(dtSeconds);
// 讀取當前時間,計算增量
U64 tEnd = readHiResTimer();
dtSeconds = (tEnd - tBegin)/ getHiRestTimerFrequency();
// tEnd為下一幀的tBegin
tBegin = tend;
}
而對於遊戲循環的方法還有許多種,筆者雖然想一個一個慢慢介紹,但礙於時間緣故就不再此多做介紹了。感興趣的朋友們可以看看這篇文章,而在之後的實作環節筆者必定會將其都整理進去的!
今天是學校的社團迎新茶會,忙來忙去因此10.才開始寫今天的鐵人。
其實在筆記中還記上了裝置輸入輸出、調試及日誌工具、記憶體管理、容器 (Containner)、文件系統這幾個想要介紹的點! 但由於時間不早了,剩下的內容將在明天繼續補齊,雖然時間表可能會因此稍微亂掉,不過遊戲引擎本身就是一個龐大的架構,筆者本來就預計會寫超過30天! 那就先這樣吧,日常勉勵一下自己。